package com.cmict.data.bpm.service.definition;

import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import com.cmict.data.bpm.dal.dataobject.definition.ExternalFormDO;
import com.cmict.data.form.api.task.dto.FormBaseDTO;
import com.cmict.data.framework.common.pojo.PageResult;
import com.cmict.data.framework.common.util.collection.CollectionUtils;
import com.cmict.data.framework.common.util.json.JsonUtils;
import com.cmict.data.framework.common.util.object.PageUtils;
import com.cmict.data.framework.common.util.validation.ValidationUtils;
import com.cmict.data.bpm.controller.admin.definition.vo.model.*;
import com.cmict.data.bpm.convert.definition.BpmModelConvert;
import com.cmict.data.bpm.enums.definition.BpmModelFormTypeEnum;
import com.cmict.data.bpm.service.definition.dto.BpmModelMetaInfoRespDTO;
import com.cmict.data.bpm.service.definition.dto.BpmProcessDefinitionCreateReqDTO;
import com.cmict.data.form.api.task.FormDataApi;
import com.cmict.data.form.api.task.dto.BpmFormDTO;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.flowable.bpmn.converter.BpmnXMLConverter;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.common.engine.impl.db.SuspensionState;
import org.flowable.common.engine.impl.util.io.BytesStreamSource;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.repository.Deployment;
import org.flowable.engine.repository.Model;
import org.flowable.engine.repository.ModelQuery;
import org.flowable.engine.repository.ProcessDefinition;
import org.flowable.form.api.FormRepositoryService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;
import org.springframework.validation.annotation.Validated;

import javax.annotation.Resource;
import javax.validation.Valid;
import java.nio.charset.StandardCharsets;
import java.util.*;

import static com.cmict.data.framework.common.exception.util.ServiceExceptionUtil.exception;
import static com.cmict.data.bpm.enums.ErrorCodeConstants.*;

/**
 * Flowable流程模型实现
 * 主要进行 Flowable {@link Model} 的维护
 *
 * @author yunlongn
 * @author awesome
 * @author jason
 */
@Service
@Validated
@Slf4j
public class BpmModelServiceImpl implements BpmModelService {

    @Resource
    private RepositoryService repositoryService;
    @Resource
    private BpmProcessDefinitionService processDefinitionService;
    @Resource
    private BpmFormService bpmFormService;
    @Resource
    private FormDataApi formDataApi;
    @Resource
    private BpmTaskAssignRuleService taskAssignRuleService;
    @Resource
    private ExternalFormService externalFormService;
    @Resource
    private FormRepositoryService formRepositoryService;

    @Override
    public PageResult<BpmModelPageItemRespVO> getModelPage(BpmModelPageReqVO pageVO, Long tenantId) {
        ModelQuery modelQuery = repositoryService.createModelQuery().modelTenantId(String.valueOf(tenantId));
        if (StrUtil.isNotBlank(pageVO.getKey())) {
            modelQuery.modelKey(pageVO.getKey());
        }
        if (StrUtil.isNotBlank(pageVO.getName())) {
            modelQuery.modelNameLike("%" + pageVO.getName() + "%"); // 模糊匹配
        }
        if (StrUtil.isNotBlank(pageVO.getCategory())) {
            modelQuery.modelCategory(pageVO.getCategory());
        }
        // 执行查询
        List<Model> models = modelQuery.orderByCreateTime().desc()
                .listPage(PageUtils.getStart(pageVO), pageVO.getPageSize());
        Map<BpmModelFormTypeEnum, List<Model>> formListMap = CollectionUtils.convertMultiMap(models, model -> {
            BpmModelMetaInfoRespDTO metaInfo = JsonUtils.parseObject(model.getMetaInfo(), BpmModelMetaInfoRespDTO.class);
            return BpmModelFormTypeEnum.typeOf(metaInfo.getFormType());
        });
        List<BpmModelPageItemRespVO> bpmModelPageItemRespVOS = Lists.newArrayList();
        formListMap.forEach((k, v) -> {
            if (k.equals(BpmModelFormTypeEnum.CUSTOM)) {
                // 获得 Form Map
                Set<Long> formIds = getBpmModelForm(v);
                Map<Long, FormBaseDTO> formMap = formDataApi.getFormMap(formIds);
                List<BpmModelPageItemRespVO> bpmModelItems = getBpmModelItems(formMap, v);
                bpmModelPageItemRespVOS.addAll(bpmModelItems);
            } else if (k.equals(BpmModelFormTypeEnum.EXTERNAL)) {
                Map<Long, FormBaseDTO> formMap = getBpmModelFormMap(v);
                List<BpmModelPageItemRespVO> bpmModelItems = getBpmModelItems(formMap, v);
                bpmModelPageItemRespVOS.addAll(bpmModelItems);
            } else {
                //其他类型表单
            }
        });
        bpmModelPageItemRespVOS.sort(Comparator.comparing(BpmModelPageItemRespVO::getCreateTime).reversed());
        return new PageResult<>((bpmModelPageItemRespVOS), modelQuery.count());
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public String createModel(@Valid BpmModelCreateReqVO createReqVO, String bpmnXml, Long tenantId) {
        checkKeyNCName(createReqVO.getKey());
        // 校验流程标识已经存在
        Model keyModel = this.getModelByKey(createReqVO.getKey(), String.valueOf(tenantId));
        if (keyModel != null) {
            throw exception(MODEL_KEY_EXISTS, createReqVO.getKey());
        }

        // 创建流程定义
        Model model = repositoryService.newModel();
        model.setTenantId(String.valueOf(tenantId));
        BpmModelConvert.INSTANCE.copy(model, createReqVO);
        // 保存流程定义
        repositoryService.saveModel(model);
        // 保存 BPMN XML
        saveModelBpmnXml(model, bpmnXml);
        return model.getId();
    }

    private Model getModelByKey(String key, String tenantId) {
        return repositoryService.createModelQuery().modelKey(key).modelTenantId(tenantId).singleResult();
    }

    @Override
    public BpmModelRespVO getModel(String id) {
        Model model = repositoryService.getModel(id);
        if (model == null) {
            return null;
        }
        BpmModelRespVO modelRespVO = BpmModelConvert.INSTANCE.convert(model);
        // 拼接 bpmn XML
        byte[] bpmnBytes = repositoryService.getModelEditorSource(id);
        modelRespVO.setBpmnXml(StrUtil.utf8Str(bpmnBytes));
        return modelRespVO;
    }

    @Override
    @Transactional(rollbackFor = Exception.class) // 因为进行多个操作，所以开启事务
    public void updateModel(@Valid BpmModelUpdateReqVO updateReqVO) {
        // 校验流程模型存在
        Model model = repositoryService.getModel(updateReqVO.getId());
        if (model == null) {
            throw exception(MODEL_NOT_EXISTS);
        }
        // 校验表单是否已绑定
        if (isBoundForm(updateReqVO, model)) {
            throw exception(FORM_IS_BOUND);
        }
        // 修改流程定义
        BpmModelConvert.INSTANCE.copy(model, updateReqVO);
        // 更新模型
        repositoryService.saveModel(model);
        // 更新 BPMN XML
        saveModelBpmnXml(model, updateReqVO.getBpmnXml());
    }

    private boolean isBoundForm(BpmModelUpdateReqVO updateReqVO, Model modelPresent) {
        ModelQuery modelQuery = repositoryService.createModelQuery();
        // 执行查询
        List<Model> models = modelQuery.orderByCreateTime().desc().list();
        // 获得 Form Map
        Set<Long> formIds = CollectionUtils.convertSet(models, model -> {
            if (model.getId().equals(modelPresent.getId())) {
                return null;
            }
            BpmModelMetaInfoRespDTO metaInfo = JsonUtils.parseObject(model.getMetaInfo(), BpmModelMetaInfoRespDTO.class);
            return metaInfo != null ? metaInfo.getFormId() : null;
        });

        if (formIds.contains(updateReqVO.getFormId())) {
            return true;
        }
        return false;
    }

    @Override
    @Transactional(rollbackFor = Exception.class) // 因为进行多个操作，所以开启事务
    public void deployModel(String id) {
        // 校验流程模型存在
        Model model = repositoryService.getModel(id);
        if (ObjectUtils.isEmpty(model)) {
            throw exception(MODEL_NOT_EXISTS);
        }
        // 校验流程图
        byte[] bpmnBytes = repositoryService.getModelEditorSource(model.getId());
        if (bpmnBytes == null) {
            throw exception(MODEL_NOT_EXISTS);
        }
        // TODO：校验流程图的有效性；例如说，是否有开始的元素，是否有结束的元素；
        //获取表单类型
//        BpmModelFormTypeEnum modelFormEnum = getModelFormType(model);
        // 校验表单已配
        BpmFormDTO form = checkFormConfig(model.getMetaInfo());
        //校验任务分配规则已配置
        taskAssignRuleService.checkTaskAssignRuleAllConfig(id);

        BpmProcessDefinitionCreateReqDTO definitionCreateReqDTO = BpmModelConvert.INSTANCE.convert2(model, form).setBpmnBytes(bpmnBytes);
        //校验模型是否发生修改。如果未修改，则不允许创建
        if (processDefinitionService.isProcessDefinitionEquals(definitionCreateReqDTO)) { // 流程定义的信息相等
            ProcessDefinition oldProcessInstance = processDefinitionService.getProcessDefinitionByDeploymentId(model.getDeploymentId());
            if (oldProcessInstance != null && taskAssignRuleService.isTaskAssignRulesEquals(model.getId(), oldProcessInstance.getId())) {
                throw exception(MODEL_DEPLOY_FAIL_TASK_INFO_EQUALS);
            }
        }
        // 创建流程定义
        String definitionId = processDefinitionService.createProcessDefinition(definitionCreateReqDTO);

        // 将老的流程定义进行挂起。也就是说，只有最新部署的流程定义，才可以发起任务。
        updateProcessDefinitionSuspended(model.getDeploymentId());

        // 更新 model 的 deploymentId，进行关联
        ProcessDefinition definition = processDefinitionService.getProcessDefinition(definitionId);
        model.setDeploymentId(definition.getDeploymentId());
        repositoryService.saveModel(model);
        // 如果是外部表单，需要部署表单
        deployExternalForm(definitionCreateReqDTO, definition.getDeploymentId());
        //复制任务分配规则
        taskAssignRuleService.copyTaskAssignRules(id, definition.getId());
    }


    private void deployExternalForm(BpmProcessDefinitionCreateReqDTO definitionCreateReqDTO, String deploymentId) {
        Integer formType = definitionCreateReqDTO.getFormType();
        if (formType.equals(BpmModelFormTypeEnum.EXTERNAL.getType())) {
            String name = definitionCreateReqDTO.getName();
            Long formId = definitionCreateReqDTO.getFormId();
            ExternalFormDO externalForm = externalFormService.getExternalForm(formId);
            formRepositoryService.createDeployment().addFormBytes(externalForm.getName(), externalForm.getConf().getBytes(StandardCharsets.UTF_8))
                    .name(name).parentDeploymentId(deploymentId).deploy();
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteModel(String id) {
        // 校验流程模型存在
        Model model = repositoryService.getModel(id);
        if (model == null) {
            throw exception(MODEL_NOT_EXISTS);
        }
        // 执行删除
        repositoryService.deleteModel(id);
        // 禁用流程实例
        updateProcessDefinitionSuspended(model.getDeploymentId());
    }

    @Override
    public void updateModelState(String id, Integer state) {
        // 校验流程模型存在
        Model model = repositoryService.getModel(id);
        if (model == null) {
            throw exception(MODEL_NOT_EXISTS);
        }
        // 校验流程定义存在
        ProcessDefinition definition = processDefinitionService.getProcessDefinitionByDeploymentId(model.getDeploymentId());
        if (definition == null) {
            throw exception(PROCESS_DEFINITION_NOT_EXISTS);
        }

        // 更新状态
        processDefinitionService.updateProcessDefinitionState(definition.getId(), state);
    }

    @Override
    public BpmnModel getBpmnModel(String id) {
        byte[] bpmnBytes = repositoryService.getModelEditorSource(id);
        if (ArrayUtil.isEmpty(bpmnBytes)) {
            return null;
        }
        BpmnXMLConverter converter = new BpmnXMLConverter();
        return converter.convertToBpmnModel(new BytesStreamSource(bpmnBytes), true, true);
    }

    private void checkKeyNCName(String key) {
        if (!ValidationUtils.isXmlNCName(key)) {
            throw exception(MODEL_KEY_VALID);
        }
    }

    /**
     * 校验流程表单已配置
     *
     * @param metaInfoStr 流程模型 metaInfo 字段
     * @return 流程表单
     */
    private BpmFormDTO checkFormConfig(String metaInfoStr) {
        BpmModelMetaInfoRespDTO metaInfo = JsonUtils.parseObject(metaInfoStr, BpmModelMetaInfoRespDTO.class);
        if (metaInfo == null || metaInfo.getFormType() == null) {
            throw exception(MODEL_DEPLOY_FAIL_FORM_NOT_CONFIG);
        }
        // 校验表单存在
        if (Objects.equals(metaInfo.getFormType(), BpmModelFormTypeEnum.CUSTOM.getType())) {
            BpmFormDTO form = formDataApi.getFormConfig(metaInfo.getFormId());

            if (form == null) {
                throw exception(FORM_NOT_EXISTS);
            }
            return form;
        } else if (Objects.equals(metaInfo.getFormType(), BpmModelFormTypeEnum.EXTERNAL.getType())) {

        }
        return null;
    }

//    /**
//     * 获取流程的表单类型
//     * @param model
//     * @return
//     */
//    private BpmModelFormTypeEnum getModelFormType(Model model) {
//        String metaInfoStr = model.getMetaInfo();
//        BpmModelMetaInfoRespDTO metaInfo = JsonUtils.parseObject(metaInfoStr, BpmModelMetaInfoRespDTO.class);
//        if (metaInfo == null || metaInfo.getFormType() == null) {
//            throw exception(MODEL_DEPLOY_FAIL_FORM_NOT_CONFIG);
//        }
//        Integer formType = metaInfo.getFormType();
//        return BpmModelFormTypeEnum.typeOf(formType);
//    }

    private void saveModelBpmnXml(Model model, String bpmnXml) {
        if (StrUtil.isEmpty(bpmnXml)) {
            return;
        }
        repositoryService.addModelEditorSource(model.getId(), StrUtil.utf8Bytes(bpmnXml));
    }

    /**
     * 挂起 deploymentId 对应的流程定义。 这里一个deploymentId 只关联一个流程定义
     *
     * @param deploymentId 流程发布Id.
     */
    private void updateProcessDefinitionSuspended(String deploymentId) {
        if (StrUtil.isEmpty(deploymentId)) {
            return;
        }
        ProcessDefinition oldDefinition = processDefinitionService.getProcessDefinitionByDeploymentId(deploymentId);
        if (oldDefinition == null) {
            return;
        }
        processDefinitionService.updateProcessDefinitionState(oldDefinition.getId(), SuspensionState.SUSPENDED.getStateCode());
    }

    private List<BpmModelPageItemRespVO> getBpmModelItems(Map<Long, FormBaseDTO> formMap, List<Model> models) {
        // 获得 Deployment Map
        Set<String> deploymentIds = new HashSet<>();
        models.forEach(model -> CollectionUtils.addIfNotNull(deploymentIds, model.getDeploymentId()));
        Map<String, Deployment> deploymentMap = processDefinitionService.getDeploymentMap(deploymentIds);
        // 获得 ProcessDefinition Map
        List<ProcessDefinition> processDefinitions = processDefinitionService.getProcessDefinitionListByDeploymentIds(deploymentIds);
        Map<String, ProcessDefinition> processDefinitionMap = CollectionUtils.convertMap(processDefinitions, ProcessDefinition::getDeploymentId);
        return BpmModelConvert.INSTANCE.convertList(models, formMap, deploymentMap, processDefinitionMap);
    }

    private Set<Long> getBpmModelForm(List<Model> models) {
        Set<Long> formIds = CollectionUtils.convertSet(models, model -> {
            BpmModelMetaInfoRespDTO metaInfo = JsonUtils.parseObject(model.getMetaInfo(), BpmModelMetaInfoRespDTO.class);
            return metaInfo != null ? metaInfo.getFormId() : null;
        });
        return formIds;
    }

    private Map<Long, FormBaseDTO> getBpmModelFormMap(List<Model> models) {
        Map<Long, FormBaseDTO> formMap = Maps.newHashMap();
        for (Model model : models) {
            BpmModelMetaInfoRespDTO metaInfo = JsonUtils.parseObject(model.getMetaInfo(), BpmModelMetaInfoRespDTO.class);
            FormBaseDTO formBase = new FormBaseDTO();
            if (metaInfo != null) {
                formBase.setId(metaInfo.getFormId());
                ExternalFormDO externalForm = externalFormService.getExternalForm(metaInfo.getFormId());
                String formName = externalForm == null ? "" : externalForm.getName();
                formBase.setFormName(formName);
            }
            formMap.put(metaInfo.getFormId(), formBase);
        }
        return formMap;
    }

}
